Skip to content

feat(pdu): add arbitrary feature for structure-aware fuzzing#1272

Merged
Benoît Cortier (CBenoit) merged 1 commit into
Devolutions:masterfrom
lamco-admin:feat/ironrdp-pdu-arbitrary
May 14, 2026
Merged

feat(pdu): add arbitrary feature for structure-aware fuzzing#1272
Benoît Cortier (CBenoit) merged 1 commit into
Devolutions:masterfrom
lamco-admin:feat/ironrdp-pdu-arbitrary

Conversation

@glamberson
Copy link
Copy Markdown
Contributor

@glamberson Greg Lamberson (glamberson) commented May 13, 2026

Summary

Closes #1121. Part of Benoît Cortier (@CBenoit)'s fuzzing umbrella #1120; this PR unblocks the downstream sub-issues #1122 (Arbitrary impls for more PDU structs), #1123 (CI feature matrix), and #1124 (advanced fuzz targets). Supersedes #1128 by rajatdahal (@razzat008) (stalled 75+ days; 2026-05-11 collaboration offer did not get a response).

Wires the arbitrary feature on ironrdp-pdu and adjusts the existing ironrdp-connector cascade so it composes correctly. This is the foundation for structure-aware fuzzing harnesses that generate valid-looking PDU inputs rather than raw bytes.

rajatdahal (@razzat008)'s exploration in #1128 was the starting point. The decisions in this PR about the ChannelName manual impl shape, the Arc<dyn LicenseCache> trade-off, the StaticChannelSet skip, and the feature-cascade wiring all build on the questions raised in that thread.

Approach

The Cargo.toml wiring uses the explicit cascade pattern:

[features]
arbitrary = ["alloc", "dep:arbitrary", "bitflags/arbitrary"]

[dependencies]
bitflags = "2.11"
arbitrary = { version = "1", features = ["derive"], optional = true }

This keeps bitflags itself free of an always-on arbitrary feature so consumers that do not opt into our arbitrary feature do not pull the arbitrary crate transitively. The cascade preserves the no_std + alloc build path the issue acceptance criterion calls for.

#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] is applied to PDU structs and enums across the gcc, mcs, nego, capability sets, client_info, server_license, autodetect, finalization_messages, session_info, suppress_output, refresh_rectangle, server_error_info, multitransport, headers, input, basic_output, codecs/rfx, and vc/dvc/gfx modules.

ChannelName gets a hand-rolled Arbitrary impl that preserves the wire-format invariant (8-byte null-terminated, up to seven ANSI characters). LicenseExchangeSequence in ironrdp-connector gets a hand-rolled impl with NoopLicenseCache as a placeholder so the license-exchange surface stays reachable under fuzz.

Alternatives considered

  • Always-on bitflags/arbitrary (as in Fix/arbitrary failing on feature flag #1128): pulls the arbitrary crate into every consumer of ironrdp-pdu unconditionally. Rejected because it defeats the optional-feature design.
  • arbitrary feature on ironrdp-svc (as in Fix/arbitrary failing on feature flag #1128): rejected per the review comment that "we're probably not going to deal with 'arbitrary static virtual channels'." The trait-object types on the connector side are skipped via #[arbitrary(default)] instead.
  • Populating StaticChannelSet with plausible channels under fuzz: considered but deferred. The skip path matches Fix arbitrary feature flag plumbing so it compiles #1121's compile-gate scope; channel-state fuzzing coverage is a follow-up evaluated after the initial fuzz runs surface what coverage looks like.

Trade-offs accepted

  • StaticChannelSet fields on ConnectionResult and ClientConnector are empty under fuzz (skip via #[arbitrary(default)]). Connector code paths that branch on populated channel state are not exercised. Tracked for coverage iteration.
  • LicenseExchangeSequence uses NoopLicenseCache under fuzz. Cache-dependent paths are not exercised. A fuzz-mode LicenseCache impl is a possible follow-up.
  • Error enums carrying foreign types (ServerLicenseError, ConnectorErrorKind) are intentionally not annotated. They are not part of the wire-protocol surface fuzzing targets.

Each trade-off is recorded with an inline source comment naming the reason.

Verification

  • cargo fmt --all -- --check clean
  • cargo check -p ironrdp-pdu --features arbitrary succeeds
  • cargo check -p ironrdp-pdu --features arbitrary,std succeeds
  • cargo check -p ironrdp-pdu --no-default-features --features arbitrary,alloc succeeds
  • cargo check -p ironrdp-pdu (default features) succeeds
  • cargo check -p ironrdp-connector --features arbitrary succeeds
  • cargo xtask check fmt clean
  • cargo xtask check lints clean (pre-existing warnings unchanged)
  • cargo xtask check typos clean

References

Comment thread fuzz/README.md Outdated
Comment thread crates/ironrdp-pdu/src/rdp/server_license/mod.rs Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR wires an optional arbitrary feature across ironrdp-pdu (and cascades it into ironrdp-connector) to enable structure-aware fuzzing by providing arbitrary::Arbitrary impls (mostly via derives) for a broad set of RDP PDU types, plus documentation on how to compile with the feature.

Changes:

  • Add arbitrary feature plumbing to ironrdp-pdu (incl. bitflags/arbitrary) and cascade it through ironrdp-connector.
  • Add #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] across many PDU structs/enums/bitflags, plus manual Arbitrary impls where needed (ChannelName, LicenseExchangeSequence).
  • Document how to build/check crates with --features arbitrary for fuzzing workflows.

Reviewed changes

Copilot reviewed 87 out of 88 changed files in this pull request and generated no comments.

Show a summary per file
File Description
fuzz/README.md Documents how to build/check crates with the arbitrary feature for structured fuzzing.
crates/ironrdp-pdu/Cargo.toml Adds arbitrary feature wiring and optional arbitrary dependency.
crates/ironrdp-pdu/README.md Documents arbitrary (and other) feature flags.
crates/ironrdp-pdu/src/gcc/mod.rs Enables Arbitrary on GCC block/container types.
crates/ironrdp-pdu/src/gcc/cluster_data.rs Adds Arbitrary derives for cluster redirection types.
crates/ironrdp-pdu/src/gcc/conference_create.rs Adds Arbitrary derives for conference create PDUs.
crates/ironrdp-pdu/src/gcc/core_data/mod.rs Adds Arbitrary derive for RdpVersion.
crates/ironrdp-pdu/src/gcc/core_data/client.rs Adds Arbitrary derives for client core data + enums/bitflags.
crates/ironrdp-pdu/src/gcc/core_data/server.rs Adds Arbitrary derives for server core data + bitflags.
crates/ironrdp-pdu/src/gcc/message_channel_data.rs Adds Arbitrary derives for message channel data.
crates/ironrdp-pdu/src/gcc/monitor_data.rs Adds Arbitrary derives for monitor data + flags.
crates/ironrdp-pdu/src/gcc/monitor_extended_data.rs Adds Arbitrary derives for extended monitor info + enums.
crates/ironrdp-pdu/src/gcc/multi_transport_channel_data.rs Adds Arbitrary derives for multi-transport channel GCC block.
crates/ironrdp-pdu/src/gcc/network_data.rs Adds manual Arbitrary for ChannelName and derives for network data types/options.
crates/ironrdp-pdu/src/gcc/security_data.rs Adds Arbitrary derives for security data structs/enums/bitflags.
crates/ironrdp-pdu/src/geometry.rs Adds Arbitrary derives for rectangle types.
crates/ironrdp-pdu/src/mcs.rs Adds Arbitrary derives for MCS PDUs/enums/structs.
crates/ironrdp-pdu/src/nego.rs Adds Arbitrary derives for negotiation flags/data types.
crates/ironrdp-pdu/src/pcb.rs Adds Arbitrary derives for preconnection blob/version types.
crates/ironrdp-pdu/src/rdp/mod.rs Adds Arbitrary derive for ClientInfoPdu.
crates/ironrdp-pdu/src/rdp/autodetect.rs Adds Arbitrary derives for auto-detect request/response enums.
crates/ironrdp-pdu/src/rdp/client_info.rs Adds Arbitrary derives for client info structs/enums/bitflags/builder.
crates/ironrdp-pdu/src/rdp/finalization_messages.rs Adds Arbitrary derives for finalization/control-related PDUs.
crates/ironrdp-pdu/src/rdp/headers.rs Adds Arbitrary derives for slow-path headers + related enums/bitflags.
crates/ironrdp-pdu/src/rdp/multitransport.rs Adds Arbitrary derives for multitransport PDUs/enums.
crates/ironrdp-pdu/src/rdp/refresh_rectangle.rs Adds Arbitrary derive for refresh rectangle PDU.
crates/ironrdp-pdu/src/rdp/server_error_info.rs Adds Arbitrary derives for server error info PDU/enums.
crates/ironrdp-pdu/src/rdp/session_info/mod.rs Adds Arbitrary derives for session info wrapper/types.
crates/ironrdp-pdu/src/rdp/session_info/logon_info.rs Adds Arbitrary derives for logon info structs.
crates/ironrdp-pdu/src/rdp/session_info/logon_extended.rs Adds Arbitrary derives for extended logon info + flags/enums.
crates/ironrdp-pdu/src/rdp/suppress_output.rs Adds Arbitrary derives for suppress output PDU + enum.
crates/ironrdp-pdu/src/rdp/vc/mod.rs Adds Arbitrary derives for VC header + control flags.
crates/ironrdp-pdu/src/rdp/vc/dvc/gfx/mod.rs Adds Arbitrary derives for GFX DVC PDUs/types.
crates/ironrdp-pdu/src/rdp/vc/dvc/gfx/graphics_messages/mod.rs Adds Arbitrary derives for GFX capability set + flags and basic types.
crates/ironrdp-pdu/src/rdp/vc/dvc/gfx/graphics_messages/client.rs Adds Arbitrary derives for GFX client PDUs/types.
crates/ironrdp-pdu/src/rdp/vc/dvc/gfx/graphics_messages/server.rs Adds Arbitrary derives for many GFX server message PDUs/enums.
crates/ironrdp-pdu/src/rdp/vc/dvc/gfx/graphics_messages/avc_messages.rs Adds Arbitrary derives for GFX AVC message types/flags.
crates/ironrdp-pdu/src/rdp/capability_sets/mod.rs Adds Arbitrary derives for capability set containers/enums.
crates/ironrdp-pdu/src/rdp/capability_sets/general/mod.rs Adds Arbitrary derives for general capability structs/flags.
crates/ironrdp-pdu/src/rdp/capability_sets/bitmap.rs Adds Arbitrary derives for bitmap capability + flags.
crates/ironrdp-pdu/src/rdp/capability_sets/bitmap_cache/mod.rs Adds Arbitrary derives for bitmap cache capability types/flags.
crates/ironrdp-pdu/src/rdp/capability_sets/bitmap_codecs/mod.rs Adds Arbitrary derives for bitmap codecs capability types/flags/enums.
crates/ironrdp-pdu/src/rdp/capability_sets/brush/mod.rs Adds Arbitrary derives for brush capability types.
crates/ironrdp-pdu/src/rdp/capability_sets/frame_acknowledge.rs Adds Arbitrary derive for frame acknowledge capability.
crates/ironrdp-pdu/src/rdp/capability_sets/glyph_cache/mod.rs Adds Arbitrary derives for glyph cache capability types.
crates/ironrdp-pdu/src/rdp/capability_sets/input.rs Adds Arbitrary derives for input capability + flags.
crates/ironrdp-pdu/src/rdp/capability_sets/large_pointer.rs Adds Arbitrary derives for large pointer capability + flags.
crates/ironrdp-pdu/src/rdp/capability_sets/multifragment_update.rs Adds Arbitrary derive for multifragment update capability.
crates/ironrdp-pdu/src/rdp/capability_sets/offscreen_bitmap_cache/mod.rs Adds Arbitrary derive for offscreen bitmap cache capability.
crates/ironrdp-pdu/src/rdp/capability_sets/order/mod.rs Adds Arbitrary derives for order capability types/flags.
crates/ironrdp-pdu/src/rdp/capability_sets/pointer.rs Adds Arbitrary derive for pointer capability.
crates/ironrdp-pdu/src/rdp/capability_sets/sound/mod.rs Adds Arbitrary derives for sound capability + flags.
crates/ironrdp-pdu/src/rdp/capability_sets/surface_commands.rs Adds Arbitrary derives for surface commands capability + flags.
crates/ironrdp-pdu/src/rdp/capability_sets/virtual_channel/mod.rs Adds Arbitrary derives for virtual channel capability + flags.
crates/ironrdp-pdu/src/rdp/server_license/mod.rs Adds Arbitrary derives for licensing PDUs/types; documents why some errors aren’t annotated.
crates/ironrdp-pdu/src/rdp/server_license/licensing_error_message/mod.rs Adds Arbitrary derives for licensing error message PDU/enums.
crates/ironrdp-pdu/src/rdp/server_license/client_new_license_request/mod.rs Adds Arbitrary derives for new license request PDU + flags.
crates/ironrdp-pdu/src/rdp/server_license/client_license_info.rs Adds Arbitrary derive for client license info PDU.
crates/ironrdp-pdu/src/rdp/server_license/client_platform_challenge_response/mod.rs Adds Arbitrary derives for platform challenge response PDU/enums.
crates/ironrdp-pdu/src/rdp/server_license/server_license_request/mod.rs Adds Arbitrary derives for server license request + subtypes.
crates/ironrdp-pdu/src/rdp/server_license/server_license_request/cert.rs Adds Arbitrary derives for certificate-related types.
crates/ironrdp-pdu/src/rdp/server_license/server_platform_challenge/mod.rs Adds Arbitrary derive for server platform challenge PDU.
crates/ironrdp-pdu/src/rdp/server_license/server_upgrade_license/mod.rs Adds Arbitrary derives for server upgrade license types.
crates/ironrdp-pdu/src/input/mod.rs Adds Arbitrary derives for input event PDU + event enum.
crates/ironrdp-pdu/src/input/fast_path.rs Adds Arbitrary derives for fast-path input types/flags (notably FastPathInput).
crates/ironrdp-pdu/src/input/mouse.rs Adds Arbitrary derives for mouse input types/flags.
crates/ironrdp-pdu/src/input/mouse_rel.rs Adds Arbitrary derives for relative mouse input types/flags.
crates/ironrdp-pdu/src/input/mouse_x.rs Adds Arbitrary derives for extended mouse input types/flags.
crates/ironrdp-pdu/src/input/scan_code.rs Adds Arbitrary derives for scan code input types/flags.
crates/ironrdp-pdu/src/input/sync.rs Adds Arbitrary derives for sync input types/flags.
crates/ironrdp-pdu/src/input/unicode.rs Adds Arbitrary derives for unicode input types/flags.
crates/ironrdp-pdu/src/input/unused.rs Adds Arbitrary derive for unused input PDU.
crates/ironrdp-pdu/src/basic_output/fast_path/mod.rs Adds Arbitrary derives for fast-path output header/update types/flags.
crates/ironrdp-pdu/src/basic_output/slow_path.rs Adds Arbitrary derives for slow-path update enums.
crates/ironrdp-pdu/src/basic_output/pointer/mod.rs Adds Arbitrary derives for pointer update data structures.
crates/ironrdp-pdu/src/basic_output/surface_commands/mod.rs Adds Arbitrary derives for surface command PDUs/enums.
crates/ironrdp-pdu/src/basic_output/bitmap/mod.rs Adds Arbitrary derives for bitmap update structures + flags.
crates/ironrdp-pdu/src/basic_output/bitmap/rdp6.rs Adds Arbitrary derives for RDP6 bitmap stream types.
crates/ironrdp-pdu/src/codecs/rfx/mod.rs Adds Arbitrary derives for RFX codec channel/block types.
crates/ironrdp-pdu/src/codecs/rfx/header_messages.rs Adds Arbitrary derives for RFX header message types.
crates/ironrdp-pdu/src/codecs/rfx/data_messages.rs Adds Arbitrary derives for RFX data message types/flags/enums.
crates/ironrdp-pdu/src/codecs/rfx/progressive.rs Adds Arbitrary derives for RFX progressive message types.
crates/ironrdp-connector/Cargo.toml Makes connector arbitrary feature cascade into ironrdp-pdu/arbitrary.
crates/ironrdp-connector/src/lib.rs Adds Arbitrary derives to several public connector types; documents excluded error enum.
crates/ironrdp-connector/src/license_exchange.rs Adds a manual Arbitrary impl for LicenseExchangeSequence due to trait object field.
crates/ironrdp-connector/src/connection.rs Skips StaticChannelSet during Arbitrary derivation via #[arbitrary(default)].
crates/ironrdp-connector/src/connection_activation.rs Adds Arbitrary derives for activation sequence/state.
Cargo.lock Updates lockfile due to new optional dependency/feature usage.
Comments suppressed due to low confidence (1)

crates/ironrdp-pdu/src/input/fast_path.rs:265

  • Deriving arbitrary::Arbitrary for FastPathInput can generate instances that violate its documented invariant (Vec length must be 1..=255). That invariant is relied on via u8::try_from(self.0.len()).expect(...) in encode()/size(), so fuzz-generated values can trigger panics unrelated to protocol logic. Consider implementing a custom Arbitrary impl (or using derive customization) that constrains the event count to 1..=255 (and ideally avoids the empty case) so encode/size remain panic-free under --features arbitrary.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown
Member

@CBenoit Benoît Cortier (CBenoit) left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I would drop Arbitrary from state machine types for now: ClientConnector, ClientConnectorState, ConnectionActivationSequence, ConnectionActivationState (connector, not PDU).
These are orchestration types, and the core problem is that both *State::Consumed variants are freely generated. Any fuzzer iteration landing in Consumed immediately gets back "connector sequence state is finalized or consumed (this is a bug)". We could use #[arbitrary(skip)], but I think we’re better off with waiting until we implement a good harness at which point it will be easier to design something relevant there.

Comment thread crates/ironrdp-pdu/src/rdp/client_info.rs Outdated
@glamberson
Copy link
Copy Markdown
Contributor Author

Fixed. The bounded-collection invariant pattern recurs in two more places in this PR's cascade, so the fix is broader than just FastPathInput:

  • FastPathInput (the named case): hand-rolled Arbitrary impl that picks len = u.int_in_range(1..=255)? then fills the Vec.
  • ConferenceCreateRequest and ConferenceCreateResponse: same class of bug. Both have a gcc_blocks.size() <= u16::MAX - ... invariant whose violation panics via u16::try_from(...).expect(...) in encode(). Hand-rolled impls now generate the inner fields then call the existing validating Self::new(...) constructor, mapping the validation Err to arbitrary::Error::IncorrectFormat so the fuzzer discards size-overflowing inputs cleanly.

Other try_from(...).expect(...) sites in the cascade were audited and confirmed not panic-prone under derive: compile-time SIZE constants, bit-range extractions, and post-validated values do not depend on Arbitrary-generated Vec lengths.

@glamberson
Copy link
Copy Markdown
Contributor Author

OK. Dropped derives on ClientConnector, ClientConnectorState, ConnectionActivationSequence, and ConnectionActivationState. Also dropped on ConnectionResult since it carries connection_activation as a field and falls in the same orchestration-output bucket. Will reconsider when #1124's state-machine oracle lands and we have a notion of valid state-sequence inputs.

Wires the `arbitrary` feature on `ironrdp-pdu` and adjusts the existing
`ironrdp-connector` cascade so it composes correctly. This unblocks
structure-aware fuzz harnesses that generate valid-looking PDU inputs
rather than raw bytes.

`ironrdp-pdu`:
- `arbitrary = ["alloc", "dep:arbitrary", "bitflags/arbitrary"]` in the
  feature table, with `bitflags = "2.11"` clean in the dependency table
  so the cascade is truly optional.
- `#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]` on
  PDU structs and enums across gcc, mcs, nego, capability_sets,
  client_info, server_license, autodetect, finalization_messages,
  session_info, suppress_output, refresh_rectangle, server_error_info,
  multitransport, headers, input, basic_output, codecs/rfx, vc/dvc/gfx.
- Hand-rolled `Arbitrary` for `ChannelName` to preserve the
  null-terminated 8-byte wire-format invariant.
- `ServerLicenseError` and similar error enums carrying foreign types
  (`io::Error`, `FromUtf8Error`, `x509_cert::der::Error`, `PduError`)
  are intentionally not annotated, with inline comments noting the
  reason.

`ironrdp-connector`:
- Cascade adjusted to `arbitrary = ["dep:arbitrary", "ironrdp-pdu/arbitrary"]`.
- `static_channels` on `ConnectionResult` and `ClientConnector` skipped
  via `#[cfg_attr(feature = "arbitrary", arbitrary(default))]` with
  inline rationale (StaticChannelSet is keyed by TypeId).
- `license_cache: Option<Arc<dyn LicenseCache>>` on `Config` skipped
  similarly with `arbitrary(default)`.
- `LicenseExchangeSequence` hand-rolled with `NoopLicenseCache`
  placeholder so the license-exchange surface stays reachable under
  fuzz, with inline note that cache-dependent paths are not exercised.
- `ConnectorErrorKind` intentionally not annotated (carries `sspi::Error`).

Documentation:
- `fuzz/README.md` gains a section on building with the `arbitrary`
  feature and the categories of types that opt out.
- `crates/ironrdp-pdu/README.md` gains a `## Feature Flags` section.

Verification matrix:
- `cargo fmt --all -- --check` clean
- `cargo check -p ironrdp-pdu --features arbitrary` succeeds
- `cargo check -p ironrdp-pdu --features arbitrary,std` succeeds
- `cargo check -p ironrdp-pdu --no-default-features --features arbitrary,alloc` succeeds
- `cargo check -p ironrdp-pdu` (default features) succeeds
- `cargo check -p ironrdp-connector --features arbitrary` succeeds
- `cargo xtask check fmt` clean
- `cargo xtask check lints` clean (warnings are pre-existing duplicates)
- `cargo xtask check typos` clean

Closes Devolutions#1121.
Copy link
Copy Markdown
Member

@CBenoit Benoît Cortier (CBenoit) left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

@CBenoit Benoît Cortier (CBenoit) merged commit af11df1 into Devolutions:master May 14, 2026
10 checks passed
@CBenoit
Copy link
Copy Markdown
Member

I opened a follow up PR: #1272

@glamberson Greg Lamberson (glamberson) deleted the feat/ironrdp-pdu-arbitrary branch May 15, 2026 02:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Fix arbitrary feature flag plumbing so it compiles

4 participants